﻿using System.Collections.Generic;
using System.Net;
using PhiStore.Packages;
using PhiStore.Threading;
using PhiStore.Util;

namespace PhiStore.Communication
{
    internal class PeerManager
    {
        private Dictionary<string, PeerContainer> peers;
        private static PeerManager instance = null;

        private object mutex = new object();

        private PeerManager()
        {
            peers = new Dictionary<string, PeerContainer>();
        }

        internal static PeerManager Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new PeerManager();
                }
                return instance;
            }
        }

        internal bool AddPeer(PeerContainer pc)
        {
            bool result = false;
            lock (mutex)
            {
                if (!peers.ContainsKey(pc.BC.ToString()))
                {
                    result = true;
                    peers[pc.BC.ToString()] = pc;
                    Log.Instance.AddLogEntry("PeerManager", "Added Peer: " + pc.ToString());
                }
                Log.Instance.AddLogEntry("PeerManager", "Peer Count is " + peers.Count);
            }
            return result;
        }

        internal long PeerCount
        {
            get
            {
                lock (mutex)
                {
                    return peers.Count;
                }
            }
        }

        internal void Change(BroadcastObject oldBC, BroadcastObject newBC)
        {
            lock (mutex)
            {
                if (peers.ContainsKey(oldBC.ToString()) && !peers.ContainsKey(newBC.ToString()))
                {
                    PeerContainer pc = peers[oldBC.ToString()];
                    pc.BC = newBC;
                    peers.Add(newBC.ToString(), pc);
                }
            }
        }
       
        internal bool RemovePeer(PeerContainer pc)
        {
            bool result = true;
            int newPeerCount = -1;
            int oldPeerCount = -1;

            List<PeerContainer> remainingPeers;
            lock (mutex)
            {
                oldPeerCount = peers.Count;
                if (pc != null && peers.ContainsKey(pc.BC.ToString()))
                {
                    pc.PeerWorkerChannel.Poison();
                    Log.Instance.AddLogEntry("PeerManager", "Connection lost to peer: " + pc.BC.ToString());
                    peers.Remove(pc.BC.ToString());
                }
            }
         

            remainingPeers = GetPeers();


            //Checking own connection && gossip 
            Channel<object> fromPeers = new Channel<object>(remainingPeers.Count);
            List<PeerContainer> badPeers = new List<PeerContainer>();
            foreach (PeerContainer p in remainingPeers)
            {
                object reply;
                try
                {
                    p.PeerWorkerChannel.Send(new ForwardedPackage(new WhoAmI(), fromPeers));
                    reply = fromPeers.Receive();
                }
                catch (PoisonException)
                {
                    reply = ReturnCode.ConnectionLost;
                }

                if (reply is ReturnCode && (ReturnCode)reply == ReturnCode.ConnectionLost)
                {
                    badPeers.Add(p);
                    Log.Instance.AddLogEntry("PeerManager", "Connection lost to peer: " + p.BC.ToString());
                }
            }

            foreach (PeerContainer peer in badPeers)
            {
                RemovePeer(peer);
            }
            newPeerCount = peers.Count;

            if (oldPeerCount != newPeerCount) //I lost connection to one/more peer(s)
            {
                foreach (PeerContainer peer in GetPeers())
                {
                    try
                    {
                        peer.PeerWorkerChannel.Send(new ForwardedPackage(new Gossip(), null));
                    }
                    catch (PoisonException)
                    {
                        //hmmm - that was a strange disconnect
                    }
                }
            }

           

            if ((oldPeerCount +1) / 2 >= (newPeerCount+1)) // this is not good - lost connection to more than n / 2 +1 in one go
            {
                Log.Instance.AddLogEntry("PeerManager", "Connection to too many peers lost at once - shutting down!");
                PhiServer.Instance.Stop();//shutting down
                result = false;
            }
            return result;
        }

        internal PeerContainer GetNewPeer(List<PeerContainer> oldpeers)
        {
            List<PeerContainer> allpeers = GetPeers(PeerCount); //all peers randomized

            foreach (PeerContainer p in allpeers)
            {
                if (!oldpeers.Contains(p))
                {
                    return p;
                }
            }
            return null;
        }

        internal List<PeerContainer> GetPeers()
        {
            List<PeerContainer> result = new List<PeerContainer>();
            lock (mutex)
            {
                result.AddRange(peers.Values);
            }
            return result;
        }

        internal List<PeerContainer> GetPeers(long count)
        {
            List<PeerContainer> result = new List<PeerContainer>();

            lock (mutex)
            {
                if (peers.Count >= count)
                {
                    PeerContainer[] allPeers = new PeerContainer[peers.Count];
                    peers.Values.CopyTo(allPeers, 0);

                    //randomize peers
                    ArrayUtil.Randomize<PeerContainer>(allPeers);

                    for (uint i = 0; i < count; i++)
                    {
                        result.Add(allPeers[i]);
                    }
                }
            }
            return result;
        }

        internal PeerContainer GetPeer(BroadcastObject bc)
        {
            lock (mutex)
            {
                return peers[bc.ToString()];
            }
        }

        internal PeerContainer GetPeer(IPAddress ip, int port)
        {
            BroadcastObject bc = new BroadcastObject(ip, port);
            return GetPeer(bc);
        }

        internal bool Exist(PeerContainer pc)
        {
            lock (mutex)
            {
                return peers.ContainsKey(pc.BC.ToString());
            }
        }

        internal IList<BroadcastObject> GetAllBroadcastObjects()
        {
            IList<PeerContainer> all = GetPeers();
            List<BroadcastObject> bcs = new List<BroadcastObject>();
            foreach (PeerContainer p in all)
            {
                bcs.Add(p.BC);
            }
            return bcs;
        }

        internal void Clear()
        {
            lock (mutex)
            {
                foreach (PeerContainer p in peers.Values)
                {
                    p.PeerWorkerChannel.Poison();
                }
                peers.Clear();
                Log.Instance.AddLogEntry("PeerManager", "Peers are cleared");
            }
        }

        internal bool Exist(IPAddress Ip, int port)
        {
            lock (mutex)
            {
                BroadcastObject bc = new BroadcastObject(Ip, port);
                return peers.ContainsKey(bc.ToString());
            }
        }
    }
}
